Rust 中的惯用错误处理方式
错误处理是任何编程语言的关键部分,而 Rust 提供了一种独特而强大的错误处理方法。与许多其他编程语言不同,Rust 没有异常机制,而是提供了 Result 枚举,这迫使开发者以一致且可预测的方式处理所有错误,从而更容易识别和诊断问题。
由于 Rust 没有异常,每个函数要么返回一个值,要么 “panic”。当一个函数 panic 时,进程会立即退出并向调用者提供一些反馈。虽然在 Rust 中可以使用 catch_unwind 技术上可以捕获 panic,但这并不推荐用于一般用途。相反,Rust 提供了 Result 枚举,强制开发者处理所有错误。
本博文将探讨 Rust 中的惯用错误处理模式,帮助你理解其基础知识。我们将介绍 Result 枚举,它在 Rust 程序中如何用于处理错误,以及一些使这个过程更简单的常用 crate。
Result 类型
如果一个函数可能失败,那么它通常会返回 Rust 的 Result 类型。当从函数返回 Result 时,开发者必须返回其两个变体之一:Result::Ok 或 Result::Err。由于这些变体非常常见,它们在 prelude 中可用,所以你可以直接写 Ok 或 Err。
由于 Result 是一个枚举,所以可以很直接地匹配错误并处理任一情况:
fn fallible(succeed: bool) -> Result<&'static str, &'static str> {
if succeed {
return Ok("success!");
}
Err("this is an error message")
}
fn main() -> Result<(), &'static str> {
let result = fallible(false);
let value = match result {
Ok(value) => value,
Err(err) => {
return Err(err);
}
};
println!("got a value: {value}");
Ok(())
}
如你所见,我们可以使用 Rust 的标准 match 运算符来分支处理任一枚举变体。在 Rust 中,函数不会抛出异常,而是可以 panic (理想情况下永远不应发生)或返回 Result 类型。这个简单的例子总是返回一个错误,但你可以想象一个更复杂的函数可能会以某种意外的方式失败。
Rust 甚至允许 main 函数返回 Result。如果 main 返回的值是一个错误,Rust 将使用错误的 Debug 表示打印错误,并以错误代码退出进程。
问号运算符
当代码库大量使用 Result 时,处理每一个错误情况可能会变得繁琐。为了克服这个负担,Rust 提供了问号运算符,它是一种解包成功结果或将错误返回给调用者的简写方式。本质上,这个问号运算符将错误传播——或者说"冒泡"——给调用者。
例如,我们可以大大简化前面的例子:
fn fallible(succeed: bool) -> Result<&'static str, &'static str> {
if succeed {
return Ok("success!");
}
Err("this is an error message")
}
fn main() -> Result<(), &'static str> {
let value = fallible(false)?;
println!("got a value: {value}");
Ok(())
}
这个例子等同于第一个,但我们使用 ? 运算符来轻松地从 Result::Ok 中提取预期的值(如果存在);否则将 Result::Error 变体返回给调用者。
错误封装
在之前的例子中,我们使用简单的字符串来返回错误。在更复杂的情况下,你可能会遇到表示可以返回的不同类型错误的错误类型。
考虑以下示例:
use reqwest::blocking::get;
fn download() -> Result<String, reqwest::Error> {
let website_text = get("https://www.rust-lang.org")?.text()?;
Ok(website_text)
}
fn main() -> Result<(), reqwest::Error> {
let value = download()?;
println!("got a value: {value}");
Ok(())
}
此示例从 https://www.rust-lang.org 下载内容,我们在这里使用 ?
操作符来简化错误处理。注意,每个 Result 的第二个类型现在是 reqwest::Error。这种类型适用,因为 get() 和 text() 返回的 Result 的错误类型均为 reqwest::Error。
现在,假设你还想将下载的网站文本存储在文件中。我们可以轻松地更新代码来完成这一操作:
use tempfile::tempfile;
use std::io::copy;
use reqwest::blocking::get;
fn download() -> Result<String, reqwest::Error> {
let mut file = tempfile()?;
let website_text = get("https://www.rust-lang.org")?.text()?;
copy(&mut website_text.as_bytes(), &mut file)?;
Ok(website_text)
}
fn main() -> Result<(), reqwest::Error> {
let value = download()?;
println!("got a value: {value}");
Ok(())
}
但这段代码无法编译!编译上述代码会产生以下错误:
|
5 | fn download() -> Result<String, reqwest::Error> {
| ------------------------------ expected `reqwest::Error` because of this
6 | let mut file = tempfile()?;
| ^ the trait `From<std::io::Error>` is not implemented for `reqwest::Error`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= help: the following other types implement trait `FromResidual<R>`:
<Result<T, F> as FromResidual<Result<Infallible, E>>>
<Result<T, F> as FromResidual<Yeet<E>>>
= note: required for `Result<String, reqwest::Error>` to implement `FromResidual<Result<Infallible, std::io::Error>>`
这个错误发生是因为 tempfile() 返回的错误类型与我们的 Result<String, reqwest::Error> 函数签名不匹配。解决这个问题最简单的方法是“封装”错误,并返回 Result<String, Box>。
标准库提供了这个 std::error::Error trait 来处理需要返回多种错误类型的情况。库暴露的错误类型应实现 Error,这样我们就可以轻松地将几种不同的错误类型转换为通用的 trait 对象。再次使用 ?
操作符可以轻松实现这一点。
use reqwest::blocking::get;
use std::error::Error;
use std::io::copy;
use tempfile::tempfile;
fn download() -> Result<String, Box<dyn Error>> {
let mut file = tempfile()?;
let website_text = get("https://www.rust-lang.org")?.text()?;
copy(&mut website_text.as_bytes(), &mut file)?;
Ok(website_text)
}
fn main() -> Result<(), Box<dyn Error>> {
let value = download()?;
println!("got a value: {value}");
Ok(())
}
还值得一提的是 anyhow,这是另一个流行的 crate,提供了类似于使用 Box> trait 对象方法的体验,并增加了一些开发者友好的功能。
使用 thiserror
使用 anyhow 或 Box> 的缺点是我们失去了函数返回的错误类型,这些错误类型通常有助于编写基于发生的错误类型的分支逻辑。
维护错误上下文的常用约定是构建一个自定义错误枚举,描述在 crate 或模块中可能发生的错误。
use reqwest::blocking::get;
use std::io::copy;
use tempfile::tempfile;
fn download() -> Result<String, Error> {
let mut file = tempfile().map_err(|_| Error::File)?;
let website_text = get("https://www.rust-lang.org")
.map_err(|_| Error::Download)?
.text()
.map_err(|_| Error::Download)?;
copy(&mut website_text.as_bytes(), &mut file).map_err(|_| Error::File)?;
Ok(website_text)
}
fn main() -> Result<(), Error> {
let value = download()?;
println!("got a value: {value}");
Ok(())
}
#[derive(Debug)]
enum Error {
File,
Download,
}
现在,调用者了解了下载函数中产生的错误类型。然而,我们需要手动使用 map_err 将错误转换为我们的自定义 Error 枚举,这再次变得繁琐且冗长。
thiserror 使将错误类型映射到您的自定义错误类型变得更加容易:
use reqwest::blocking::get;
use std::io::copy;
use tempfile::tempfile;
use thiserror::Error;
fn download() -> Result<String, Error> {
let mut file = tempfile()?;
let website_text = get("https://www.rust-lang.org")?.text()?;
copy(&mut website_text.as_bytes(), &mut file)?;
Ok(website_text)
}
fn main() -> Result<(), Error> {
let value = download()?;
println!("got a value: {value}");
Ok(())
}
#[derive(Debug, Error)]
enum Error {
#[error("file error: {0}")]
File(#[from] std::io::Error),
#[error("download error: {0}")]
Download(#[from] reqwest::Error),
}
注意我们如何使用 thiserror 提供的宏声明性地保持错误映射到我们的自定义枚举,而我们的代码保持简洁,能够使用 ?
操作符简单地传播错误。
结论
错误处理在任何编程语言中都是至关重要的,Rust 的错误处理提供了一种强大的方法。与许多其他编程语言不同,Rust 根本不使用异常,而是依赖 Result 枚举强制开发者处理所有错误。Rust 错误处理的这种方法不仅更可靠和有效,而且也使开发更加愉快。Result 枚举使得理解和处理代码中的错误条件变得简单,这是 Rust 错误处理系统的一个主要优势。